Storing Simple Preferences Using DataStore in Android
Course: Android App Development
Topic: Storing Simple Preferences Using DataStore
Course Link: Android App Development Training
Demo Link: Register for a Course Demo
Website: JustAcademy
1. Introduction to DataStore
Android applications often need to remember small pieces of information between app sessions. Examples include a user's preferred theme, language, notification settings, onboarding completion status, or whether a particular feature is enabled. Jetpack DataStore provides a modern way to store this kind of data persistently.
Preferences DataStore stores information as key-value pairs. It uses Kotlin coroutines and Flow to perform asynchronous data operations, helping applications avoid blocking the main UI thread. It is commonly used instead of the older SharedPreferences API for simple application settings.
2. Learning Objectives
- Understand what Jetpack DataStore is and why it is useful.
- Differentiate Preferences DataStore from Proto DataStore and SharedPreferences.
- Add the DataStore dependency to an Android project.
- Create typed preference keys for different values.
- Save, read, update, and remove preferences.
- Use Kotlin Flow and coroutines to observe and update stored data.
- Integrate preferences into a Jetpack Compose screen.
- Apply best practices for error handling, architecture, and data security.
3. What Is Preferences DataStore?
Preferences DataStore is part of Android Jetpack. It stores small values using named keys, with each key associated with a supported data type. The data remains available when the application is closed and reopened, unless it is cleared or the app's data is removed.
For example, a settings screen may store the following values:
| Preference |
Data Type |
Example Value |
Purpose |
| Dark theme |
Boolean |
true |
Remember the selected theme. |
| Username |
String |
student01 |
Remember a non-sensitive display name. |
| Font size |
Int |
18 |
Store a preferred text size. |
| Last sync time |
Long |
1720000000000 |
Store a timestamp. |
| Playback speed |
Float |
1.5 |
Remember a media preference. |
| Notifications enabled |
Boolean |
false |
Remember a notification setting. |
4. Types of DataStore
4.1 Preferences DataStore
Preferences DataStore is suitable for simple key-value settings. You do not need to define a schema or create a data class for every preference. However, it does not provide the same schema-based type safety as Proto DataStore.
4.2 Proto DataStore
Proto DataStore stores structured objects defined through a schema, commonly using Protocol Buffers. It is appropriate when an application needs a well-defined data model and stronger schema-based guarantees.
| Feature |
Preferences DataStore |
Proto DataStore |
| Storage model |
Key-value pairs |
Structured typed objects |
| Schema required |
No |
Yes, through a defined model/schema |
| Best suited for |
Theme, language, toggles, small settings |
Structured application configuration |
| Learning complexity |
Lower |
Higher |
5. DataStore vs SharedPreferences
SharedPreferences is an older Android API for storing key-value pairs. Preferences DataStore provides an asynchronous Flow-based API and transactional updates, making it a strong default choice for new simple-preference implementations.
| Feature |
SharedPreferences |
Preferences DataStore |
| Reading data |
Getter methods and listeners |
Flow-based data stream |
| Writing data |
Editor with apply() or commit() |
Suspending edit() function |
| Threading model |
Some operations can cause main-thread performance issues |
Designed for asynchronous I/O |
| Updates |
Editor-based updates |
Transactional updates |
| Error handling |
Limited error reporting in common usage |
Can expose storage and I/O errors through Flow |
| Ideal use |
Existing legacy implementations |
Modern small settings and preferences |
6. Add the DataStore Dependency
Open the app-level build.gradle.kts file and add the Preferences DataStore library to the dependencies block. Use a version compatible with your project's Android Gradle Plugin and Kotlin setup.
dependencies {
implementation("androidx.datastore:datastore-preferences:1.3.0")
}
If your project uses Groovy Gradle syntax, write the dependency as follows:
dependencies {
implementation "androidx.datastore:datastore-preferences:1.3.0"
}
After adding the dependency, sync the project with Gradle. If your project already uses a version catalog, you can declare the library there and reference it from the module dependencies.
7. Create a Preferences DataStore Instance
Create a Kotlin file named AppPreferences.kt. Declare the DataStore delegate at the top level so that the application can reuse a single instance for this file.
package com.example.preferencesdemo
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStore
val Context.dataStore: DataStore<Preferences> by
preferencesDataStore(name = "app_settings")
Explanation:
Context.dataStore is an extension property that exposes the DataStore instance.
preferencesDataStore() creates a delegate for Preferences DataStore.
app_settings is the logical name used for the stored preferences file.
- The top-level declaration helps avoid accidentally creating multiple DataStore instances for the same file.
Keep the declaration in one shared location rather than declaring a new delegate inside every Activity, ViewModel, or composable.
8. Define Preference Keys
A preference key identifies a stored value. Choose the key factory that matches the type of value you need to save.
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
object PreferenceKeys {
val USERNAME = stringPreferencesKey("username")
val DARK_MODE = booleanPreferencesKey("dark_mode")
val FONT_SIZE = intPreferencesKey("font_size")
val LAST_SYNC_TIME = longPreferencesKey("last_sync_time")
}
| Key Factory |
Value Type |
Typical Use |
stringPreferencesKey() |
String |
Language code or display name |
booleanPreferencesKey() |
Boolean |
Theme or notification toggle |
intPreferencesKey() |
Int |
Font size or selected tab index |
longPreferencesKey() |
Long |
Timestamp or large counter |
floatPreferencesKey() |
Float |
Volume or playback preference |
doublePreferencesKey() |
Double |
Decimal-valued setting |
stringSetPreferencesKey() |
Set<String> |
A small collection of selected options |
9. Save a Simple Preference
To save a preference, use the suspending edit() function. It provides a mutable preferences object in which values can be added or changed.
import android.content.Context
import androidx.datastore.preferences.core.edit
suspend fun saveUsername(context: Context, username: String) {
context.dataStore.edit { preferences ->
preferences[PreferenceKeys.USERNAME] = username
}
}
Because edit() is a suspending function, call it from a coroutine or another suspending function. Do not block the main thread with a synchronous wait.
10. Save Multiple Preferences
You can update several related settings inside a single edit transaction.
suspend fun saveAppSettings(
context: Context,
username: String,
darkMode: Boolean,
fontSize: Int
) {
context.dataStore.edit { preferences ->
preferences[PreferenceKeys.USERNAME] = username
preferences[PreferenceKeys.DARK_MODE] = darkMode
preferences[PreferenceKeys.FONT_SIZE] = fontSize
}
}
Grouping related changes in one transaction helps prevent observers from seeing intermediate states between separate writes.
11. Read Preferences Using Flow
DataStore exposes its stored values through a Flow. A Flow can emit updated values as preferences change, allowing the UI or ViewModel to react to updates.
import android.content.Context
import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
fun usernameFlow(context: Context): Flow<String> =
context.dataStore.data.map { preferences ->
preferences[PreferenceKeys.USERNAME] ?: "Guest"
}
The expression ?: "Guest" provides a default when the username has not been saved yet. Similar defaults can be used for other preferences.
fun darkModeFlow(context: Context): Flow<Boolean> =
context.dataStore.data.map { preferences ->
preferences[PreferenceKeys.DARK_MODE] ?: false
}
fun fontSizeFlow(context: Context): Flow<Int> =
context.dataStore.data.map { preferences ->
preferences[PreferenceKeys.FONT_SIZE] ?: 16
}
12. Update an Existing Preference
Saving a value with an existing key replaces the previous value for that key.
suspend fun updateDarkMode(
context: Context,
enabled: Boolean
) {
context.dataStore.edit { preferences ->
preferences[PreferenceKeys.DARK_MODE] = enabled
}
}
For example, if dark mode was previously set to false, calling this function with true updates the stored preference. A collector of the corresponding Flow can receive the new value.
13. Remove a Preference
Use remove() to delete one key from the stored preferences without deleting the other settings.
suspend fun removeUsername(context: Context) {
context.dataStore.edit { preferences ->
preferences.remove(PreferenceKeys.USERNAME)
}
}
To clear every preference in this DataStore file, use clear(). Be careful because this removes all values in the file, not just one setting.
suspend fun clearAllPreferences(context: Context) {
context.dataStore.edit { preferences ->
preferences.clear()
}
}
14. Handle DataStore Errors
DataStore can report I/O problems when reading from storage. A Flow can use the catch operator to handle expected I/O exceptions and provide a fallback value where appropriate.
import androidx.datastore.core.IOException
import androidx.datastore.preferences.core.emptyPreferences
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
fun safeDarkModeFlow(context: Context) =
context.dataStore.data
.catch { exception ->
if (exception is IOException) {
emit(emptyPreferences())
} else {
throw exception
}
}
.map { preferences ->
preferences[PreferenceKeys.DARK_MODE] ?: false
}
If your IDE does not resolve IOException from the DataStore package, use java.io.IOException instead, as commonly shown in DataStore examples. Do not silently suppress every exception: unexpected errors should be allowed to propagate or be handled intentionally by the application's error-reporting strategy.
15. Use DataStore with a ViewModel
In a well-structured Android application, storage operations belong in a repository or data layer. The ViewModel exposes observable state to the UI, while the composable screen displays that state and sends user actions to the ViewModel.
15.1 Create a Repository
import android.content.Context
import androidx.datastore.preferences.core.edit
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
class UserPreferencesRepository(
private val context: Context
) {
val darkMode: Flow<Boolean> =
context.dataStore.data.map { preferences ->
preferences[PreferenceKeys.DARK_MODE] ?: false
}
suspend fun setDarkMode(enabled: Boolean) {
context.dataStore.edit { preferences ->
preferences[PreferenceKeys.DARK_MODE] = enabled
}
}
}
15.2 Create a ViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
class SettingsViewModel(
private val repository: UserPreferencesRepository
) : ViewModel() {
val darkMode = repository.darkMode.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = false
)
fun setDarkMode(enabled: Boolean) {
viewModelScope.launch {
repository.setDarkMode(enabled)
}
}
}
This example assumes the ViewModel receives its repository through dependency injection or a ViewModel factory. A production app should configure that construction explicitly instead of expecting Android to instantiate a ViewModel with arbitrary constructor parameters automatically.
16. Display and Update Preferences in Jetpack Compose
Jetpack Compose can observe the ViewModel's state and display a switch for a saved dark-mode preference. Add the lifecycle Compose integration dependency if it is not already included in your project.
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@Composable
fun SettingsScreen(viewModel: SettingsViewModel) {
val darkMode by viewModel.darkMode.collectAsStateWithLifecycle()
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Dark Mode",
style = MaterialTheme.typography.titleLarge
)
Switch(
checked = darkMode,
onCheckedChange = { enabled ->
viewModel.setDarkMode(enabled)
}
)
}
}
When the user changes the switch, the ViewModel requests an update. DataStore persists the new value, and the Flow emits the updated state. Compose then recomposes the relevant UI. For a complete theme implementation, connect the preference to your app's theme at the appropriate root composable.
17. Practical Example: Remembering a Username
Consider a learning application that asks the user for a display name. The app should remember the name after it is closed and opened again.
- Define a String key named
username.
- Save the text entered by the user using
dataStore.edit().
- Expose the saved name as a Flow.
- Provide a default such as
Guest when no value exists.
- Collect the Flow in a ViewModel and display it in the UI.
- Test the result by restarting the app and verifying that the saved name remains available.
This approach works well for non-sensitive display preferences. Do not use a plain preference key as a substitute for secure credential storage.
18. Common Errors and Troubleshooting
| Problem |
Possible Cause |
Solution |
| Unresolved DataStore imports |
Missing dependency or Gradle sync issue |
Check the module dependency and sync Gradle. |
| Value returns a default |
The key has never been saved or a different key name is being used |
Check the key declaration and the write function. |
| Write function gives a coroutine error |
A suspending function was called outside a coroutine |
Call it from a coroutine scope or another suspending function. |
| UI does not update |
Stored Flow is not collected or the UI uses a stale value |
Observe the Flow through the ViewModel and lifecycle-aware collection. |
| Multiple DataStore declarations |
Separate instances are created for the same file |
Centralize the delegate and reuse the same instance. |
| Data appears missing after reinstall |
App data was cleared or the app was reinstalled |
Understand the lifecycle of local app storage and backups. |
| Crash when reading data |
Unexpected I/O or data-related failure |
Handle expected I/O errors and investigate unexpected exceptions. |
19. Best Practices
- Prefer Preferences DataStore for small settings such as theme, language, and UI toggles.
- Declare a single DataStore delegate for each file and share it throughout the app.
- Keep persistence logic in a repository or data layer rather than writing directly inside composables.
- Use Kotlin Flow to observe stored values and coroutines to perform updates.
- Supply sensible defaults for preferences that may not have been set.
- Use descriptive and consistent key names.
- Group related updates in a single transaction where appropriate.
- Handle expected I/O failures without hiding unexpected errors.
- Use Room for relational, large, or complex datasets that need queries and relationships.
- Do not store passwords, authentication tokens, or other secrets as ordinary Preferences DataStore values. DataStore does not encrypt stored values by default.
- Test both first-run behavior and behavior after the app has saved preferences.
20. When Should You Use DataStore?
| Requirement |
Recommended Storage |
Reason |
| Remember dark mode |
Preferences DataStore |
Simple Boolean preference. |
| Remember selected language |
Preferences DataStore |
Simple String preference. |
| Save a structured settings object |
Proto DataStore or another suitable typed DataStore |
Provides a defined data model. |
| Store thousands of related records |
Room |
Supports database queries and relationships. |
| Store highly sensitive credentials |
Purpose-built secure credential storage |
Ordinary DataStore preferences are not encrypted by default. |
21. Practice Exercise
Task: Build a Settings screen that remembers a user's preferred language and dark-mode selection.
Requirements:
- Create a Preferences DataStore instance named
user_settings.
- Define a String key for the language and a Boolean key for dark mode.
- Write repository functions to read and update both preferences.
- Expose the values through a ViewModel.
- Build a Compose screen containing a language selector and a dark-mode switch.
- Run the app, change both settings, close the app, and reopen it to verify persistence.
- Add appropriate fallback values and handle expected I/O errors.
22. Interview Questions and Answers
Q1. What is Jetpack DataStore?
DataStore is an Android Jetpack solution for storing small preferences or structured objects asynchronously and transactionally.
Q2. What is Preferences DataStore?
It is the key-value implementation of DataStore, suitable for simple settings without requiring a predefined schema.
Q3. Why is DataStore often preferred over SharedPreferences?
It provides an asynchronous API based on coroutines and Flow, transactional updates, and a clearer way to propagate storage errors.
Q4. What is the purpose of preferencesDataStore()?
It creates a delegate for accessing a Preferences DataStore instance associated with a named file.
Q5. What does edit() do?
It runs a suspending transaction that provides mutable preferences, allowing keys to be added, changed, or removed.
Q6. Why does DataStore expose a Flow?
A Flow emits stored data over time, allowing consumers to react to preference changes rather than reading only a one-time snapshot.
Q7. How can a default value be provided?
Use the Elvis operator, for example preferences[DARK_MODE] ?: false.
Q8. Can DataStore store complex database records?
It is not designed for large relational datasets. Room is usually a better choice when an app needs database queries, relationships, or partial record updates.
Q9. Is Preferences DataStore encrypted by default?
No. Do not treat it as secure storage for passwords or tokens. Use an appropriate security solution for sensitive information.
Q10. Where should DataStore logic live in a Compose application?
Prefer a repository or data layer accessed by a ViewModel, then collect the exposed state in Compose using lifecycle-aware collection.
23. Quick Revision
- Preferences DataStore stores small key-value settings.
- Use typed key factories such as
stringPreferencesKey() and booleanPreferencesKey().
- Use
dataStore.edit { } to save, update, or remove values.
- Use
dataStore.data and Flow operators to read preferences reactively.
- Use coroutines for suspending writes.
- Keep storage logic in a repository and expose state through a ViewModel.
- Use lifecycle-aware collection in Jetpack Compose.
- Choose Room for complex relational data and avoid storing secrets in unencrypted preferences.
24. Official Learning Resources
Conclusion
Preferences DataStore is a useful solution for persisting simple Android application settings such as theme choices, language selections, and UI preferences. By combining typed keys, transactional writes, Kotlin coroutines, and Flow, developers can build settings features that remain available across app sessions and update the UI reactively. For maintainable applications, keep persistence in a repository, expose state through a ViewModel, and use the storage solution that matches the size, structure, and sensitivity of the data.
Explore the course: Android App Development Training | Register for a Demo | JustAcademy Homepage